=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\=/=\
============
SEH
============
Этот материал не претендует на какую-либо новизну, продвинутость в техническом
плане или что-то еще. Это простая попытка снова систематезировать все знания (в
частности мои) про SEH и все что с ним связано. Я попытаюсь рассказать все как
можно подробнее, чтобы люди неопытные, олько начинающие изучать данную тему,
тоже что-нибудь поняли. Если кратко, то этот туториал расскажет вам (я надеюсь
конечно) про то, как грамотно использовать SEH в ваших вирусах/программах и как
заставить работать ваши многомегобайтные приложения правильно и без ошибок.И еще
раз повторю: материал для людей начинающих. Я вот заметил такую интересную вещь:
когда что-то людям объясняешь, от этого сам начинаешь лучше разбираться :)
!! ВАЖНО !! Я сразу скажу, что в этом туториале по моему недосмотру или
недопониманию могут присутствовать ошибки.Поэтому не поленитесь написать мне про
эти самые недочеты. Я, как и вы, человек, и как и вы тоже пока что учусь.
!! ВАЖНО!! В исследованиях и при написании этого туториала использовалась
статья "Win32 Exception handling for assembler programmers" by Jeremy Gordon.
Поэтому в тексте будут встречаться откровенные куски авторского текста,
переведенные на русский язык и немного дополненные. Хотя я думаю, что это не
должно возобладать.
Итак, что же все-таки это за слово такое страшное - SEH? Сея аббревиатура
расшифровывается как Structured Exception Handling и переводится как
"Структурная Обработка Исключений" (вольный перевод конечно :). Все его зовут
SEH, вот и мы не будем отступать от принятых традиций.
Сам SEH присутствует в системе всегда, а в ваших программах тем более. Когда
вызывается обработчик исключений? Ну... существует большое количество правил,
нарушив которые, ваше приложение вылетит с некрасивым MessageBox`ом и красным
значком на лбу. И самое распространенное из них - это доступ туда, куда вам
лазить не разрешается. Например, память. Рассмотрим такой случай:
xor eax, eax
mov dword ptr [eax], 0
Итак, что в этом случае происходит? Ваша программа во второй строке попытается
записать дворд по адресу ds:0, куда вам, как можно было догадаться, писать не
разрешается. Ядро обработает ваш запрос и вместо того, чтобы выполнить вашу
инструкцию, в ответ вернет ошибку. А точнее: ошибку с номером 0C0000005h.И такие
инструкции, как ни странно, по недосмотру встречаются довольно часто. Примеров
на самом деле может быть огромное множество: начиная от деления на 0 и
заканчивая недостатком физической памяти или при недоступности страницы
виртуальной памяти. И действительно, насколько кривы наши руки, настолько и
разнообразнее мир всевозможных ошибок windows! Чтобы избавиться от таких вот
недоразумений, нужно грамотно использовать SEH.
Рассмотрим подробнее принцип функционирования этой хорошей вещи. Для начала на
этапе выполнения программы вам надо, собственно, этот обработчик
проинсталлировать в систему.Т.е. сказать винде куда отдавать управление в случае
возникшей ошибки. Сделать это можно разными способами, которые будут рассмотрены
дальше. Конечно, можно возложить всю возню с обработкой ошибок на саму систему,
но как мы уже заметили раньше, все что она делает -это аварийно завершает работу
приложения (путем вызова ExitProcess) и выводит непонятное сообщение. Да и
вообще. Если вот вирус вывалится таким способом - это, наверное, не очень
приятно. Особенно юзеру.
Да, таких вот обработчиков может быть большое количество. Виндовс постепенно
отдает управление каждому из них. Т.е. существует возможность настроить
отдельную процедуру обработки ошибок для каждого вашего созданного процесса и
для всех существующих ниток. На самом деле это очень удобно, но слишком
геморно :) Но не об этом речь.
Так что же делать этой самой процедуре обработки ошибок? Да все что угодно.
Хоть отдайте управление назад в программу и никто вам ничего не скажет. Только
вот потом никто за последствия отвечать не будет.
Вы можете сами анализировать код ошибки и реагировать на все по-разному. В
одном случае аварийно завершить выполнение программы (как делает винда), а в
другом тихо-мирно продолжать работу. Что касается вирусов, то тут конечно без
вопросов лучше просто молча отдать управление основной программе и удалиться.
Т.к. что-то пытаться исправлять в чужой среде - это не есть хорошо. Хотя каждый
делает по-своему... Вся прелесть в том, что если вы знаете ЧТО произошло в вашей
программе не правильно, то можете с легкостью эту ситуацию исправить не на этапе
компиляции, а уже на этапе выполнения в памяти. Это одна из самых сильных сторон
SEH. Для этого он, собственно, и создавался.
Но не все так замечательно, как кажется на первый взгляд. Существуют и такие
случаи, когда система и не додумается до того, что произошла ошибка, хотя на
самом деле такое имело место быть. Например: вы пишете цикл обработки текстовой
строки (допустим, обработка командной строки). На ваш взгляд вы все сделали
правильно и программа выполнилась верно. А теперь представим такую ситуацию: вы
не предусмотрели выход из цикла, если искомая комбинация символом не была
найдена. Т.е. ваш кусок кода будет выполняться до тех пор, пока не найдет нужную
ему строчку, а такой строчки нет. Что тогда? Да ничего интересного. Программа
просто повиснет в бесконечном цикле, а виндовс даже не догадается, что ваше
приложение делает что-то неправильное. Как ни странно, здесь SEH оказывается
бессильным.
Или еще ситуация: программа работает на удивление правильно и без ошибок. Но
вдруг происходит внутренний системный сбой (так называемый BSOD: Blue Screen Of
Death) - и все данные потеряны. Вот в таком случае не поможет не то чтобы SEH,но
и Ctrl+Alt+Delete не всегда срабатывает :) Итак, делаем вывод: SEH защищает
программиста от ЕГО ошибок (почти всегда), но не от ошибок системы. Стоит ли
использовать обработчики исключений? Мой ответ - да! Хотя бы для того, чтобы не
потерять данные приложения в случае непредвиденных ситуаций.
Получив некоторые представления о рассматриваемой теме, поедем дальше. Ближе к
коду. Но сначала давайте посмотрим на то, что делает виндовс во время того,когда
произошло исключение. Итак, по-порядку:
1) Винда обнаруживает факт произошедшей ошибки и посылает код этой самой
ошибки программе. Если программа находится под дебаггером, то винда услужливо
уведомляет сей дебаггер, посылая ему EXCEPTION_DEBUG_EVENT. Таким образом
отладчики могут корректно обрабатывать некорректно работающие программы (во как
сказал :)
2) Если программа не отлаживается, то винда пытается уведомить уже ваш
обработчик исключений, так называемый меж-нитевой обработчик (per-thread
exception handler).Он инсталлируется программой во время выполнения и его адрес
находится в первом дворде TIB`а (Thread Information Block), который в свою
очередь лежит по адресу fs:[0]. Как я уже говорил выше, существует возможность
создавать отдельную процедуру обработки ошибок для каждой нитки. Вот в этом
месте все это и происходит. Код ошибки передается последовательно каждой
процедуре.
3) Если такого обработчика по этому адресу не существует, то система вызывает
последнюю возможную процедуру, которая указывает на то место кода, которое может
грамотно завершить работу приложения. Она так же во время выполнения
инсталлируется руками программиста, но уже через специальную API:
SetUnhandledExceptionFilter.
4) Если и этого винда не обнаружила, то выводится тот самый известный
MessageBox с красным некрасивым значком и насильственно вызывается ExitProcess.
Но перед этим система позакрывает все,что принадлежало программе и почистит
стек.
Таким образом существует четырехуровневая попытка добиться от вашей программы
правильной работы и не загадить своими остатками всю систему. Удивительно! Как
все же трогательно виндовс заботится о нас! :)
Но нет предела чувству юмора людей из Microsoft. Вся документация про SEH в
MSDN`е заточена под тех, кто пишет на С/C++. Нет ничего тупее и кривее, чем
обработка исключений на Си :) Нет ничего тормознее всех тех API, которые
используются в этом процессе.Но мы-то тупить не будем.Ведь у нас есть ассемблер.
Вот мы и подошли к самому интересному. А именно к реализации этого всего в
наших программах.
Но начнем мы все-таки с самого неинтересного и ненужного, а именно с API
SetUnhandledExceptionFilter. Единственный параметр, которая она принимает - это
адрес процедуры обработки исключений. Эта процедура будет защищать весь код
вашего приложения от начала и до конца, т.е. нет возможности с помощью этой API
настраиваться на отдельные нитки (чем она и непримечательна). Рассмотрим
реализацию этого метода:
================================================================
start:
push offset handler
call SetUnhandledExceptionFilter
; Тут идет код вашей программы, защищенный с помощью обработчика исключений
push 0
call ExitProcess
;-------------------
; А тут начинается сама процедура обработки
handler:
; Какие-то важные действия, без которых программа завершится не так как нужно
; Например, код, который закрывает все открытые ресурсы программы (файлы,
; описатели, ...)
mov eax, 1
ret
===============================================================
Все по-моему понятно и объяснять что-то дополнительно не требуется (я так
думаю). А, вот еще что. В eax может содержаться три значения при выходе:
0 - виндовс покажет слощастный MessageBox с
информацией об ошибке
1 - винда не будет показывать никаких сообщений
0ffffffffh - есть возможность перегрузить контекст и
продолжить
И еще одно в догонку: если вы где-то в середине кода своей программы снова
вызовите эту API, то старый адрес обработчика просто заменится на новый и все.
Дальше - лучше. Второй тип обработчика исключений, применяемый для защиты
отдельных кусков вашего кода разными способами.
Так называемый "per-thread exception handler". Давайте разберемся подробнее как
все это происходит на деле.
Итак, каждая ваша новая нитка содержит новое значение сегментного регистра fs,
а так как при инсталяции этого обработчика в систему для работы требуется именно
этот регистр, то не сложно сделать вывод о возможности защиты этих самых ниток
разными процедурами. В какой области кода произойдет ошибка, то будет вызванна
именно та процедура, которая отвечает за этот участок и никакая другая.Тем самым
перед нами открывается неограниченные возможности управления нашими программами.
Например, если какая-то нитка вашего процесса "вышла их строя",можно с легкостью
создать новую на основе имеющихся данныхо произошедшей ошибке. Или динамически
изменять обработчики ниток в зависимости от обстоятельств, или ...да мало ли что
еще.
Главное в этом методе -это гибкость и повышенная отказоустойчивость программы.
А уж каким образом вы распорядитесь этими возможностями - ваше дело.
По адресу fs:[0] лежит дворд, указывающий на системную структуру TIB (Thread
Information Block).Эта структура содержит важную информацию о каждой нитке вашей
программы. А вот первый дворд в TIB указывает на другую структуру, содержащую
информацию об обработчиках исключений, которая нам и нужна. Она имеет такой вид:
Смещение Размер Описание
+0 dword Указатель на следующую структуру обработчика
+4 dword Указатель на процедуру обработки исключений
Рассмотрим реализацию этого метода:
===========================================
; Заполняем структуру обработчика исключений
push offset handler ; Адрес процедуры обработчика
push dword ptr fs:[0] ; Адрес следующей структуры обработчика
mov dword ptr fs:[0], esp ; Заносим в fs:[0] адрес созданной
; нами выше структуры
; Тут идет код программы, защищенный обработчиком, определенным выше
pop dword ptr fs:[0] ; Возвращаем все в исходное состояние
add esp, 4
ret
; А тут начинается сама процедура обработки ошибок
handler:
; Код процедуры
mov eax, 1 ; Если eax = 1, то винда обратится к следующему
; определенному обработчику (если надо)
; Если eax = 0, существует возможность перегрузить
; контекст и продолжить выполнение
ret
=========================================
Тоже на мой взгляд все просто, но давайте все же разберемся. В первых строках
нашей программы мы создаем в стеке новую структуру обработки исключений, чтобы
положить ее по адресу fs:[0] (причем наша процедура будет вызвана первой).Первым
делом заносим в стек адрес нашей процедуры обработки ошибок, второй push - адрес
предыдущей процедуры обработки (неверняка там будет обработчик, определенный
где-то в ядре). Т.е. создаем точно такую же структуру, которую я описал раньше.
После выполнения нашей программы мы должны убрать все, что насоздавали в
системе. Таким образом мы помещаем на прежнее место (fs:[0]) адрес старого
обработчика и выравниваем стек. Все гениальное просто! :)
Следует не забывать правильно все инсталлировать. Система постепенно пройдется
по всем определенным структурам (в документации это называется chaining) и
выберет то место, которое отвечает за участок кода где произошла ошибка.
В этих процедурах может содержаться все что угодно на ваш вкус. Но в самом
конце мы должны занести в eax одно из предусмотренных значений: 1 или 0. По этим
значениям виндовс будет знать что делать дальше.
А теперь что касается непосредственно вирусов. SEH - незаменимая вещь при
написании вируса. Во-первых, это помогает скрыть свое присутствие в зараженном
файле путем грамотной обработки возникшей неприятности. Во-вторых, существует
замечательная возможность поиска адреса кернела в памяти системы. Теперь же мы
знаем, что при загрузке программы в пямять, виндовс автоматически инициализирует
некий обработчик исключений, определенный где-то в недрах ядра. Нам же остается
найти этот адрес (тем же способом, какой был показан выше при инсталляции нашего
приложения) и найти само ядро. Вот вам пример:
===========================================
virus:
mov esi, fs:[0] ; Сохраним предыдущий SEH
push offset handler ; Настроим свой SEH
push dword ptr fs:[0]
mov dword ptr fs:[0], esp
Seacrh_KrnAddr:
cmp dword ptr [esi], 0ffffffffh ; Если это последний элемент SEH
je KrnAddr_Ok ; то его обрабатывает kernel - поэтому у
mov esi, [esi] ; нас есть его адрес, иначе берем адрес
; следующего обработчика
jmp Seacrh_KrnAddr ; и ищем дальше
KrnAddr_Ok:
mov esi, [esi+4] ; Возьмем адрес kernel`a из SEH
and esi, 0ffff0000h ; округлим его до страницы
mov ecx, 100h ; Предел возможного кол-ва поиска (256 раз)
; А дальше на самом деле идет стандартный метод поиска заголовка
; ядра, описанного во многих туториалах. Здесь я ради экономия места
; этого помещать не буду
===============================================
До метки Seacrh_KrnAddr уже должно быть все кристально ясно :) А вот дальше
стоит пояснить. Дело все в том, что последний элемент SEH (определенный самим
ядром) будет на месте следующего элемента обработчика содержать -1 (0ffffffffh).
То есть следующего элемента нет и быть не может. Этим-то мы и воспользуемся,
учитывая то, что на месте адреса обработчика будет лежать какой-то адрес дебрей
системы. Нам останется только его округлить до страницы (т.к. адреса модулей в
памяти выровнены именно на эту границу) и работать дальше ни о чем не
задумываясь.
Теперь давайте посмотрим на то, как же нам узнать какая ошибка произошла с
нашим приложением. Как только произошло исключение, система помещает в стек
информацию об этом событии и отдает управление в процедуру обработчика (если она
есть). Т.к. существует два способа инсталляции этой процедуры (с помощью API и с
помощью fs:[0] ... как мы уже знаем), из этого следует, что существует и два
варианта реакции системы.
Итак, по-порядку (как обычно с самого неинтересного). Если обработчик был
объявлен с помощью SetUnhandledExceptionFilter, то в стеке будет содержаться
адрес структуры EXCEPTION_POINTERS, определенной так:
Смещение Размер Описание
+0 dword Указатель на структуру EXCEPTION_RECORD
+4 dword Указатель на структуру CONTEXT_RECORD
Первая структура содержит информацию о случившейся ошибке и ее в основном и
надо использовать при анализе возникшей неполадки. А вот вторая структура
содержит всю процессорно-зависимую информацию (регистры и всякую другую хрень),
нужную в основном только для отладки и для отладчика. Рассмотрим их подробнее:
EXCEPTION_RECORD structure:
---------------------------
Смещение Размер Название Описание
+0 dword ExceptionCode Код возникшей ошибки
+4 dword ExceptionFlag Флаги, которые может использовать
процедура обработки
+8 dword NestedExceptionRecord Адрес другой структуры
EXCEPTION_RECORD, если
обработчик во время работы
сам вызвал какую-то ошибку :)
+С dword ExceptionAddress Адрес в коде где случилась ошибка.
Нужная вещь!
+10 dword NumberParameters Количество двордов в следующем поле
+14 dword Additional information Массив из двордов, содержащий
дополнительную информацию об
ошибке
Описание:
---------
- ExceptionCode: код возникшей ошибки.Может содержать как коды программных, так
и аппаратных шибок. Их достаточно много (они в документации и в заголовочных
файлах все определены). Вот некоторые, которые могут на самом деле встретиться
вам:
C0000005h - Невозможность чтения/записи определенного участка памяти (наш
самый первый пример)
C0000094h - Деление на 0
C00000FDh - Переполнение стека
C0000025h - Ошибка, которая не должна обрабатываться нашей процедурой ( и
так бывает)
C0000026h - Ошибка, которая используется самой системой при обработке
нашей ошибки.
80000003h - Используется отладчиками: выполнился бряк при int3
80000004h - Используется отладчиками: идет пошаговое выполнение программы
Произвольное значение (не определенное системой) может использоваться вами для
того, чтобы прямо вывалится в обработчик исключений.Это нужно для того, чтобы вы
смогли потестить созданную процедуру.
- ExceptionFlag: три флага, анализируя которые процедура обработки может делать
выводы о серьезности ошибки. Возможны следующие значения:
0 - Ошибка, после исправления которой возможно продолжение работы вашего
приложения
1 - Ошибка, не позволяющая дальнейшее выполнение программы
2 - Не надо пытаться реагировать на эту ошибку. Может использоваться во
время того, когда чистится стек, используемый другими обработчиками.
Все остальные значения должны быть понятными из описания. Теперь что касается
CONTEXT_RECORD. Блин, это самая здоровая структура, которую я только видел =)
Причем для разных процессоров она разная (все это определено в winnt.h). Для
примера опишу поля этого монстра для интеловских процессоров (IA32 - Intel 386
и старше):
CONTEXT_RECORD structure:
-------------------------
Смещение Размер Название Описание
+0 dword context flags Флаги контекста. Используются
при вызове GetThreadContext
Регистры для отладки:
+4 dword debug register #0 Регистры нужные для отладки #0-7
+8 dword debug register #1
+C dword debug register #2
+10 dword debug register #3
+14 dword debug register #6
+18 dword debug register #7
Регистры MMX и для работы с числами с плавающей точкой:
+1C dword ControlWord Если честно - без понятия что они значат :)
Смотрите документацию
+20 dword StatusWord
+24 dword TagWord
+28 dword ErrorOffset
+2C dword ErrorSelector
+30 dword DataOffset
+34 dword DataSelector
+38 50 FP registers x 8 (10 байт каждый)
+88 dword Cr0NpxState
Сегментные регистры:
+8C dword gs register Сегментные регистры данных
+90 dword fs register
+94 dword es register
+98 dword ds register
Регистры:
+9C dword edi register Основные регистры
+A0 dword esi register
+A4 dword ebx register
+A8 dword edx register
+AC dword ecx register
+B0 dword eax register
Контрольные регистры: И остальные регистры
+B4 dword ebp register
+B8 dword eip register
+BC dword cs register
+C0 dword eflags register
+C4 dword esp register
+C8 dword ss register
Так что вот так вот. Огромное количество информации, если грамотно обработать
которую, можно с легкостью исправить возникшие неприятности. На самом деле все
что вам понадобится, содержится в первой структуре в первом ее поле. Все
остальное только для извращенцев и для отладчика.
И при втором варианте (инсталляции обработчика с помощью сегмента fs) в стек
кладется такая информация:
Смещение Размер Описание
esp+4 dword Адрес структуры EXCEPTION_RECORD
esp+8 dword Адрес нашей структуры обработчика исключений, описанной выше
esp+c dword Адрес труктуры CONTEXT_RECORD
Первая и третья структуры, как вы наверное уже заметили, были описаны выше. А
вот на счет второй нужно поговорить поподробнее. На самом деле это та же
структура, которую мы заполняли при инсталляции в самой начале программы. Когда
мы помещали адрес этой структуры в стек, мы указали на ее начало.Т.е. существует
возможность расширить ее до некоторых размеров, чтобы система могла заполнить
несколько дополнительных полей нужной информацией.Сколько не ругайся, а все-таки
умные ребята в Microsoft работают =) Итак, наша структура может выглядеть
следующим образом ([esp+8] как раз указывает на ее вершину):
Смещение Размер Описание
+0 dword Адрес следующей структуры обработчика исключений
+4 dword Адрес нашей процедуры обработки
+8 dword Адрес в программе, содержащий так называемое
"безопасное место"
+с dword Информация для нашей процедуры от системы
+10 dword Флаги
+14 dword Значение ebp в нашем безопасном месте
Приведем кусок кода, показывающего преимущества такого расширения. Т.к. нашей
программе может понадобиться некоторые переменные, к которым она не сможет
получить доступ при ошибке, то мы еще выделим место в стеке и для них.
===============================
; Стандартная инициализация локальных переменных:
push ebp ; Сохраним ebp
mov esp, ebp ; Теперь будем использовать ebp как указатель вершины стека
sub esp, 40h ; Выделим 16 двордов под нужные нам переменные
; Заполняем нашу новую структуру обработки исключений:
push ebp ; Сохраним ebp
push 0 ; Место под флаги, которые передаст нам windows
push 0 ; Место под информацию для нашего обработчика
push offset safe_place ; Адрес "безопасного места" (будет помещен в eip)
push offset handler ; Адрес нашей процедуры обработки
push dword ptr fs:[0] ; Адрес предыдущего обработчика
mov dword ptr fs:[0], esp ; И сохраним адрес созданной выше структуры
; Тут идет код, защищенный нашей процедурой
jmp _exit ; Если все прошло без ошибок
safe_place: ; Если все же произошла ошибка
; Код этого самого безопасного места. Виндовс настоит
; регистры eip/esp/ebp как вы и просили
_exit:
pop dword ptr fs:[0] ;Восстановим все как было
mov esp, ebp
pop ebp
ret
handler:
; Наш новый обработчик
ret
==================================
Что же мы имеем на данный момент? Модифицировав нашу структуры мы добились
того, что при возникновении ошибки мы уже не тупо завершаем программу, а имеем
полный контроль над ситуацией. Выделив область под локальные переменные, есть
возможность не потерять важные данные при сбое программы, а правильно их
обработать и сохранить. Так же у нас есть доступ к системной информации, которая
может и должна помогать при решении конфликтных ситуаций. Короче, прям как самый
настоящий отладчик! :)
Вообще, если все это дело хорошо понять, то можно писать достаточно устойчивые
к сбоями приложения, что в наши дни уже становится редкостью. Намечается легко
понимаемая тенденция в программировании: чем больше размер кода программы
(возьмите стандартные виндосовские программы с графическим интерфейсом и
обработкой данных), тем сложнее его отлаживать и находить ошибки.И как мы знаем,
неисправности могут быть такого типа, что их еще и искать замучаешься. Поэтому
предлагаю всем, кто дочитал до этого места разобраться в вышеописанной
технологии (а надо признать, что здесь Microsoft превзошла саму себя и сделала
действительно ХОРОШУЮ вещь :) и писать нормальные программы, способные грамотно
обрабатывать как свои, так и чужие ошибки.
Я думаю это того стоит ... Аминь!
НЕ РАССМОТРЕННЫЕ ТЕМЫ:
- Во-первых,не рассмотрены способы, использующие в своей работе несколько ниток
одновременно. Возможно в будущем этот текст будет немного исправлен и
дополнен. Ждите обновлений.
- Во-вторых не были рассмотрены примеры восстановления работы программы во
время ее выполнения. Я думаю, если вы поняли вышележащий текст, то сами
способны это осуществить.
Конструктивную критику можно слать по адресу: [email protected] dzen ([email protected])[ Prev Page | Goto Content | NextPage ]